const net = require('net')
const dns = require('dns')
const { assert } = require('console')
const uuid = require('uuid')

const utils = require('./utils')
const { writeFileSync } = require('fs')
const consts = require('./consts')()
const config = require('./config')()

function Proxy(socket) {
	return {
		/**
		 * proxy socket
		 */
		_socket: socket,
		/**
		 * session
		 */
		_session: {
			id: uuid.v1(),
			buffer: Buffer.alloc(0),
			offset: 0,
			state: consts.STATE.METHOD_NEGOTIATION,
		},
		/**
		 * 客户端连接到服务器，并发送版本标识符/方法选择消息
     * +----+----------+----------+
     * |VER | NMETHODS | METHODS  |
     * +----+----------+----------+
     * | 1  |    1     | 1 to 255 |
     * +----+----------+----------+
		 * 客户端应用程序连接上服务器，发送协议版本号、客户端支持的认证方法列表给服务器。上面注释中数字代表字节长度，比如 VER 为 1 字节长，后文中出现类似的数字都代表字节长度，不再赘述。
		 * - VER 字段是 SOCKS 协议版本号，传 X'05'，1 字节长
		 * - NMETHODS 字段是客户端支持的认证方法数量，每种认证方法用 1 字节进行编码，所以也决定了 METHODS 字段的长度
		 * - METHODS 依次写入支持的认证方法编码
		 */
		parseMethods() {
			let buf = this._session.buffer
			let offset = this._session.offset
			const checkNull = offset => {
				return typeof buf[offset] === undefined
			}

			if (checkNull(offset)) {
				return false
			}
			let socksVersion = buf[offset++]
			assert(socksVersion == consts.SOCKS_VERSION, `socket ${this._session.id} only support socks version 5, got [${socksVersion}]`)
			if (socksVersion !== consts.SOCKS_VERSION) {
				this._socket.end()
				return false
			}
			if (checkNull(offset)) {
				return false
			}
			let methodLen = buf[offset++]
			assert(methodLen >= 1 && methodLen <= 255, `socket ${this._session.id} methodLen's value [${methodLen}] is invalid`)

			if (checkNull(offset + methodLen - 1)) {
				return false
			}

			let methods = []
			for (let i = 0; i < methodLen; i++) {
				let method = consts.METHODS.get(buf[offset++])
				if (!!method) {
					methods.push(method)
				}
			}

      console.log(`socket ${this._session.id} SOCKS_VERSION: ${socksVersion}`)
      console.log(`socket ${this._session.id} METHODS: `, methods)
			this._session.offset = offset
			return methods
		},

		selectMethod(methods) {
			let method = consts.METHODS.NO_ACCEPTABLE
			for (let i = 0; i < methods.length; i++) {
				if (methods[i] == config.auth_method) {
					method = config.auth_method
				}
			}

      console.log(`SELECT METHOD [${method}]`)

			this._session.method = method

			return method
		},
		/**
		 * 服务器从methods中给出的方法中进行选择，并发送METHOD选择消息
		 *
     * +----+--------+
     * |VER | METHOD |
     * +----+--------+
     * | 1  |   1    |
     * +----+--------+
		 * - VER 字段是 SOCKS 协议版本号，传 X'05'，1 字节长
		 * - METHOD 字段是服务器选择的认证方法编码, 1 字节长
		 *
		 * 下面定义了认证方法的编码 `x'00'` 代表 1 字节长的十六进制数字
		 *
		 * - X'00' NO AUTHENTICATION REQUIRED // 不需要认证
		 * - X'01' GSSAPI // GSSAPI 认证，在 [RFC 1961](https://www.rfc-editor.org/rfc/rfc1961) 里规定
		 * - X'02' USERNAME/PASSWORD // 用户名密码认证，在 [RFC 1929](https://www.ietf.org/rfc/rfc1929.txt) 里规定
		 * - X'03' to X'7F' IANA ASSIGNED // 未分配
		 * - X'80' to X'FE' RESERVED FOR PRIVATE METHODS // 保留
		 * - X'FF' NO ACCEPTABLE METHODS // 服务器发现客户端上报的方法列表都不合适时回复 X'FF'
		 *
		 * 一般服务器必须实现 GSSAPI 方法，建议实现 USERNAME/PASSWORD。比较安全的环境也可以不用认证。

     * @param {*} method auth method selected
		 */
		replyMethod(method) {
			const data = Buffer.from([consts.SOCKS_VERSION, method[0]])
			console.log(data)
			this._socket.write(data)
		},
		/**
		 * 这从客户端生成用户名/密码请求开始
		 *
     * +----+------+----------+------+----------+
     * |VER | ULEN |  UNAME   | PLEN |  PASSWD  |
     * +----+------+----------+------+----------+
     * | 1  |  1   | 1 to 255 |  1   | 1 to 255 |
     * +----+------+----------+------+----------+
		 */
		parseUsernamePasswd() {
			let buf = this._session.buffer
			let offset = this._session.offset
			let req = []

			const checkNull = offset => {
				return typeof buf[offset] === undefined
			}

			if (checkNull(offset)) {
				return false
			}

			let authVersion = buf[offset++]
			assert(authVersion == consts.USERNAME_PASSWD_AUTH_VERSION,
        `socket ${this._session.id} only support auth version ${consts.USERNAME_PASSWD_AUTH_VERSION}, got [${authVersion}]`)
			if (authVersion !== consts.USERNAME_PASSWD_AUTH_VERSION) {
				this._socket.end()
				return false
			}

			if (checkNull(offset)) {
				return false
			}

			let uLen = buf[offset++]
			assert(uLen >= 1 && uLen <= 255, `socket ${this._session.id} got wrong ULEN [${uLen}]`)
			if (uLen >= 1 && uLen <= 255) {
				if (checkNull(offset + uLen - 1)) {
					return false
				}
				req.username = buf.slice(offset, offset + uLen).toString('utf8')
				offset += uLen
			} else {
				this._socket.end()
				return false
			}

			if (checkNull(offset)) {
				return false
			}
			let pLen = buf[offset++]
			assert(pLen >= 1 & pLen <= 255,  `socket ${this._session.id} got wrong PLEN [${pLen}]`)
			if (pLen >= 1 && pLen <= 255) {
				if (checkNull(offset + pLen -1)) {
					return false
				}
				req.passwd = buf.slice(offset, offset + pLen).toString('utf8')
				offset += pLen
			} else {
				this._socket.end()
				return false
			}
			this._session.offset = offset
			return req
		},
		/**
		 * 服务器验证提供的UNAME和PASSWD，并发送以下响应
		 *
     *  +----+--------+
     *  |VER | STATUS |
     *  +----+--------+
     *  | 1  |   1    |
     *  +----+--------+
		 */
		replyAuth(succeeded) {
			let reply = [
				consts.USERNAME_PASSWD_AUTH_VERSION,
				succeeded ? consts.AUTH_STATUS.SUCCESS : consts.AUTH_STATUS.FAILURE
			]
			if (succeeded) {
				this._socket.write(Buffer.from(reply))
			} else {
				this._socket.end(Buffer.from(reply))
			}
		},
		/**
		 * 客户端认证成功后发送请求信息
		 * 请求格式如下：
     * +----+-----+-------+------+----------+----------+
     * |VER | CMD |  RSV  | ATYP | DST.ADDR | DST.PORT |
     * +----+-----+-------+------+----------+----------+
     * | 1  |  1  | X'00' |  1   | Variable |    2     |
     * +----+-----+-------+------+----------+----------+
		 * - VER 字段是 SOCKS 协议版本号，传 X'05'，1 字节长
		 * - CMD 字段是请求命令，1字节长
		 *   - CONNECT X'01' 代表连接目标服务器命令，本文会实现 CONNECT 请求命令
		 *   - BIND X'02' 暂时不介绍
		 *   - UDP ASSOCIATE X'03' 暂时不介绍
		 * - RSV 保留字节，传 X'00'，1字节长
		 * - ATYP 目标地址类型，1字节长
		 * 		- IP V4 address: X'01' IPV4
		 * 		- DOMAINNAME: X'03' 域名
		 *    - IP V6 address: X'04' IPV6
		 * - DST.ADDR 客户端想要请求的目标地址
		 * - DST.PORT 客户端想连接的目标端口号，2 字节长，网络字节序
		 *
		 * DST.ADDR 字段根据地址类型不同，长度不同。
		 *
		 * - 如果IPV4 是固定 4 字节长。
		 * - 如果是域名，则首个字节代表域名的长度（字节数），接下来的可变长度是域名字符串
		 */
		parseRequests() {
			let buf = this._session.buffer
			let offset = this._session.offset

			let req = {}

			const checkNull = offset => {
				return typeof buf[offset] === undefined
			}

			if (checkNull(offset)) {
				return false
			}

			let socksVersion = buf[offset++]
			assert(socksVersion === consts.SOCKS_VERSION, `socket ${this._session.id} only support socks version 5, got [${socksVersion}]`)

			if (socksVersion !== consts.SOCKS_VERSION) {
				this._socket.end()
				return false
			}

			if (checkNull(offset)) {
				return false
			}

			req.cmd = consts.REQUEST_CMD.get(buf[offset++])
			if (!req.cmd || req.cmd !== consts.REQUEST_CMD.CONNECT) {
				// 不支持的 cmd || 暂时只支持 connect
				this._socket.end()
				return false
			}
			if (checkNull(offset)) {
				return false
			}

			req.rsv = buf[offset++]
			assert(req.rsv === consts.RSV, `socket ${this._session.id} rsv should be ${consts.RSV}`)
			if (checkNull(offset)) {
				return false
			}
			req.atyp = consts.ATYP.get(buf[offset++])
			if (!req.atyp) {
				// 不支持 atyp
				this._socket.end()
				return false
			} else if (req.atyp === consts.ATYP.IPV4) {
				let ipLen = 4
				if (checkNull(offset + ipLen - 1)) {
					return false
				}
				req.ip = `${buf[offset++]}.${buf[offset++]}.${buf[offset++]}.${buf[offset++]}`
			} else if (req.atyp === consts.ATYP.FQDN) {
				if (checkNull(offset)) {
					return false
				}
				let domainLen = buf[offset++]
				if (checkNull(offset + domainLen - 1)) {
					return false
				}
				req.domain = buf.slice(offset, offset + domainLen).toString('utf8')
				offset += domainLen
			} else {
				// 其他暂时不支持
				this._socket.end()
				return false
			}
			let portLen = 2
			if (checkNull(offset + portLen - 1)) {
				return false
			}
			req.port = buf.readUint16BE(offset)
			offset += portLen
			console.log(`socket ${this._session.id} parse requests succeeded`, req)
			this._session.offset = offset
			return req
		},
		dstConnect(req) {
			let dstHost = req.domain || req.ip
			dns.lookup(dstHost, { family: 4 }, (e, ip) => {
				if (e || !ip) {
					// failure reply
					let reply = [
						consts.SOCKS_VERSION,
						consts.REP.HOST_UNREACHABLE[0],
						consts.RSV,
						consts.ATYP.IPV4[0],
					]
					.concat(utils.ipbytes('127.0.0.1')) // ip: 127.0.0.1
					.concat([0x00, 0x00]) // port: 0x0000

					// close connection
					this._socket.end(Buffer.from(reply))
				} else {
					// connnect target host
					const dstSocket = net.createConnection({
						port: req.port, // port from client's requests
						host: ip, // ip from dns lookup of socks proxy server
					})
					let i = 1
					dstSocket.on('connect', () => {
						// success reply
						let bytes = [
							consts.SOCKS_VERSION,
							consts.REP.SUCCEEDED[0],
							consts.RSV,
							consts.ATYP.IPV4[0],
						]
						// dstSocket.localAddress or default 127.0.0.1
						.concat(utils.ipbytes(dstSocket.localAddress || '127.0.0.1'))
						// default port 0x00
						.concat([0x00, 0x00])
						console.log(`dst localAddress: `, dstSocket.localAddress, dstSocket.localPort)
						const reply = Buffer.from(bytes)
						// use dstSocket.localPort override default port 0x0000
						reply.writeUint16BE(dstSocket.localPort, reply.length - 2)
						this._socket.write(reply)

						this._socket.pipe(dstSocket).pipe(this._socket)
						// this._socket.pipe(dstSocket).pipe(process.stdout)
					}).on('data', buf => {
						writeFileSync(`./proxy_buf${i++}`, buf.toString())
						// console.log(buf.toString())
					}).on('error', e => {
            console.error(`socket ${this._session.id} -> dstSocket`, err)
					}).on('end', () => {
            console.log(`socket ${this._session.id} -> dstSocket end`)
					}).on('close', () => {
            console.log(`socket ${this._session.id} -> dstSocket close`)
					})

					// save dstSocket to session
					this._session.dstSocket = dstSocket
				}
			})
		},
		/**
		 * called by socket's 'data' event listener
     * @param {Buffer} buf data buffer
		 */
		handle(buf) {
			// 在代理转发阶段之前，否则什么都不做
			if (this._session.state < consts.STATE.PROXY_FORWARD) {
				// 追加数据到session.buffer
				this._session.buffer = Buffer.concat([this._session.buffer, buf])

				// 丢弃处理过的字节并进行下一阶段
				const discardProcessdBytes = (nextState) => {
					this._session.buffer = this._session.buffer.slice(this._session.offset)
					this._session.offset = 0
					this._session.state = nextState
				}

				switch(this._session.state) {
					case consts.STATE.METHOD_NEGOTIATION: {
						let methods = this.parseMethods()
						// 读取完整数据
						if (!!methods) {
							let method = this.selectMethod(methods)
							this.replyMethod(method)
							switch(method) {
								case consts.METHODS.USERNAME_PASSWD:
									discardProcessdBytes(consts.STATE.AUTHENTICATION)
									break
								case consts.METHODS.NO_AUTH:
									discardProcessdBytes(consts.STATE.REQUEST_CONNECT)
									break
								case consts.METHODS.NO_ACCEPTABLE:
									this._socket.end()
									break
								default:
									this._socket.end()
							}
						}
						break
					}
          // curl www.baidu.com --socks5 127.0.0.1:3000 --socks5-basic --proxy-user  oiuytre:yhntgbrfvedc
					case consts.STATE.AUTHENTICATION: {
						// 添加 gssapi 支持
						// 需要检查 this._session.method 来解析数据
						let userinfo = this.parseUsernamePasswd()
						if (!!userinfo) { // 读完整数据
							let succeeded = (
								userinfo.username == config.username &&
								userinfo.passwd == config.passwd
							)
							discardProcessdBytes(succeeded ? consts.STATE.REQUEST_CONNECT : consts.STATE.AUTHENTICATION)
							this.replyAuth(succeeded)
						}
						break
					}
					case consts.STATE.REQUEST_CONNECT: {
						let req = this.parseRequests()
						if (!!req) { // 读完整数据
							this.dstConnect(req)
							discardProcessdBytes(consts.STATE.PROXY_FORWARD)
						}
						break
					}
					case consts.STATE.PROXY_FORWARD:
					default:
            console.log(`handle state [${this._session.state}]`, this._session)
				}
			}
		}
	}
}

module.exports = server => {
	return Proxy
}
